Skip to main content

Vue3基础知识

新特性

  • 向后兼容
  • 新特性
  • breaking change
  • 性能提升
    • 大小 -41%
    • 初次渲染快 +51%
    • 更新 +133%
    • 内存减少 -54%
  • typescript 支持

Composition API

为什么

  • 随着功能的增长,复杂组件代码难以维护

    • Vue2 api 通过一些列的 object 组织代码,缺少一种比较干净的在多个组件之间提取和复用的机制

      // vue2 的实现一个复杂功能,代码可能会很分散
      export default {
      data {
      // 复杂功能1的数据
      // 复杂功能2的数据
      },
      methods: {
      // 复杂功能1的实现方法
      // 复杂功能2的实现方法
      },
      computed: {
      // 复杂功能1
      // 复杂功能2
      },
      filters: {
      // ...
      }
      }
      // 修改一个功能,可能就要在文件中修改 data, methods, computed, 甚至 filter 等
    • 使用 mixin 解决复用的问题

      • 不知道mixin暴露的对象中的数据是什么,方法是什么、返回是什么。对象是有一定封闭性的。
        • mixin中同样会有 data、computed、methods等,逻辑依然会分散
      • 命名冲突导致覆盖问题
  • vue2 对 Typescript 的支持有局限

解决方法,使用函数

  • 按逻辑组织代码,一个功能对应一个函数,代码组织更集中
  • 在普通情况下,函数使用的参数、返回值更易查找。
  • 在 ts 的加持下,函数参数和返回值的提示更友好。

setup

不用像 vue2 那样,写 data, computed, methods 等分散的逻辑。而将这些写在 setup 方法中。setup 在props, data, computed, methods, 生命周期函数运行之前运行的,不能获得 this。

import { defineComponent } from 'vue'
export default defineComponent ({
setup() {
return ;
}
});

ref 生成响应式变量

<template>
<h1>
{{ count }}
</h1>
<a herf="javascript:;" @click="addCount">add</a>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent ({
setup() {
const count = ref(0);
const addCount = () => {
count.value++; // 使用引用类型,修改一个,其他也会更新
}
return {
count,
addCount
};
}
});
</script>

computed 计算属性

<template>
<h1>{{ count }}</h1>
<p>{{ double }}</p>
<a herf="javascript:;" @click="addCount">add</a>
</template>
<script>
import { defineComponent, ref, computed } from 'vue'
export default defineComponent ({
setup() {
const count = ref(0); // 返回 ref 对象
const double = computed(() => count.value * 2); // double 是 computedRef 对象
return {
count,
double
};
}
});
</script>

reactive 生成响应式对象

<template>
<p>Name: {{ person.name }}</p>
<p>Age: {{ person.age }}</p>
<a herf="javascript:;" @click="person.change">change name</a>
</template>
<script>
import { defineComponent, reactive } from 'vue'
export default defineComponent ({
setup() {
const person = reactive({
name: 'viking',
age: 20,
change() {
person.name = 'maomao';
person.age = 30;
}
})
return {
person
};
}
});
</script>
  • 注意:将 reactive 后的对象属性取出,会丧失响应性

toRefs

接受 reactive 对象,返回新的对象,但对象的每一项属性,都变成了 ref 类型实例(响应式的)

<template>
<p>Name: {{ name }}</p>
<p>Age: {{ age }}</p>
<a herf="javascript:;" @click="change">change name</a>
</template>
<script>
import { defineComponent, reactive, toRefs } from 'vue'
export default defineComponent ({
setup() {
const person = reactive({
name: 'viking',
age: 20,
change() {
person.name = 'maomao';
person.age = 30;
}
});
const person2 = toRefs(person);
return {
...person2
};
}
});
</script>

生命周期

也是在 setup 中使用

beforeCreatesetup 之前运行,created 是在 setup 后运行,可以直接把 beforeCreatecreated 中的逻辑,写在 setup 之中。所以这两个生命周期不再需要

深入响应式对象

  1. 保存未来执行修改的代码(effect)
  2. 监测值得改变
    1. 使用 proxy 对象实现
  3. 值改变后,执行 trigger effect

监测值得改变

使用 proxy 拦截对目标对象的读取:

const person = {
name: 'Jone'
}
const handler = {
// 目标对象,属性名,代理对象
get(target, prop, receiver) {
console.log('trigger get')
return target[prop]
}
// 目标对象,属性名,要填入的值,代理对象
set(target, prop, value, receiver){
console.log('trigger set')
traget[prop] = value
return true // 告诉设置成功
}
}
const proxy = new Proxy(person, handler)

使用 Reflect 对象上的静态方法改写:

const person = {
name: 'Jone'
}
const handler = {
get() {
console.log('trigger get')
return Reflect.get(...arguments) // Reflect.get() 接收的参数和 handler.get 接收的参数一样
}
set(){
console.log('trigger set')
return Reflect.set(...arguments) // Reflect.set() 接收的参数和 handler.set 接收的参数一样
}
}
const proxy = new Proxy(person, handler)

上面两种方式,执行效果一样

存储和触发effect

  • 将所有 effect 加入特定的数据结构
  • 创建特定的函数可以再次运行这些 effect
  • 使用 Proxy 的 getter 和 setter,将这些函数放入对应的位置
let product = { price: 5, count: 2 }
let total = 0
let dep = new Set()
function track() {
dep.add(effect)
}
function trigger() {
dep.forEach(effect => effect())
}
const reactive = (obj) => {
const handler = {
get() {
let result = Reflect.get(...arguments)
track()
return result
},
set() {
let result = Reflect.set(...arguments)
trigger()
return result
}
}
return new Proxy(obj, handler)
}
const product = reactive({ price: 5, count: 2 })
let effect = () => {
total = product.price * product.count
}
console.log(total)
product.price = 10
console.log(`total is ${total}`)

副作用

纯函数

  • 相同的输入,永远会得到相同的输出

  • 没有副作用

    const double = x => x*2; // 给定一个 x ,输出的值永远都是一样的
    Math.random(); // 不是一个纯函数

副作用

函数外部环境发生的交互

  • 网络请求
  • DOM 操作
  • 订阅数据来源
  • 写入文件系统
  • 获取用户输入

Vue 副作用处理

import { defineComponent, watchEffect } from 'vue'
export default defineComponent {
props: {
msg: string
}
setup(props) {
watchEffect(() => {
console.log('props effect', props.msg);
})
}
}
  • 组件第一次初始化时,会触发 watchEffect
  • watchEffect 回调里面的值,没有发生变化,就不会触发副总用

深入 watchEffect

  • 自动收集依赖且触发

  • 自动销毁 effect

    • 在 setup 或生命周期钩子函数中使用 watchEffect ,在组件销毁时,副作用也会一起销毁

    • 可以手动清除

      const stop = watchEffect(() => {
      console.log('props effect', props.msg);
      });
      stop(); // 销毁
  • 使副作用失效

    • watchEffect 中发送请求时,多次变化,势必多次请求

    • watchEffect 向回调中提供参数,以停止未完成的副作用

      watchEffect((onInvalidate) => {
      console.log('props effect', props.msg);
      console.log('inner effect', count.value);
      const source = axios.CancelToken.source()
      axios.get(`https://jsonplaceholder.typicode.com/todos/${count.value}`, {
      cancelToken: source.token
      }).catch(err => {
      console.log(err.message);
      });
      onInvalidate(() => {
      source.cancel('trigger');
      });
      });
  • 副作用执行顺序

    • watchEffect 都是异步执行的

    • watchEffect 先执行

    • DOM updated 再执行

      <template>
      <h1 ref="node">{{msg}}</h1>
      <h1>{{count}}</h1>
      <a herf="javascript:;" @click="count++">change</a>
      </template>
      <script>
      import { defineComponent, ref } from 'vue'
      export default defineComponent ({
      props: {
      msg: string
      }
      setup() {
      const node = ref<null | HTMLElement>(null);
      watchEffect(() => {
      const currentText = node.value ? node.value.innerText : '';
      console.log(currentText);
      }, {
      flush: 'post' // 默认是 pre:之前,
      })
      return {
      node
      };
      }
      });
      </script>
      • watchEffect 提供参数 flush 改变执行顺序。post 表示,在 DOM updated 之后再执行 watchEffect
    • React 的执行顺序不可以调整,都是在组件 updated 之后触发

watch 精准控制 effect

<template>
<h1 ref="node">{{msg}}</h1>
<h1>{{count}}</h1>
<a herf="javascript:;" @click="count++">change</a>
</template>
<script>
import { ref, watch, toRefs } from 'vue'
export default defineComponent ({
props: {
msg: string
}
setup(props) {
const node = ref<null | HTMLElement>(null);
// 基本使用
watch(count, (newV, oldV) => {
console.log(count.value)
});
// 响应式对象的值
// watch(props.msg, (newV, oldV) => {});// props 是只读的,拿出其中的值,这个值将不再是响应式对象
// 方法 1
const { msg } = toRefs(props.msg);
watch(msg, (newV, oldV) => {});
// 方法 2
watch(() => props.msg, (newV, oldV) => {});

// watch 多个值
watch([() => props.msg, count] => props.msg, (newV, oldV) => {
console.log(newV.value) // [第一个值,第二个值]
});
return {
node
};
}
});

watch 的基本用法

watch(count, (newV, oldV) => {
console.log(count.value)
});

watch 响应式对象的单个值

  • 使用 toRefs
  • 使用 getter 函数
// watch(props.msg, (newV, oldV) => {});// props 是只读的,拿出其中的值,这个值将不再是响应式对象
// 方法 1
const { msg } = toRefs(props.msg);
watch(msg, (newV, oldV) => {});
// 方法 2
watch(() => props.msg, (newV, oldV) => {});

watch 多个值

  • 使用数组
// watch 多个值
watch([() => props.msg, count] => props.msg, (newV, oldV) => {
console.log(newV.value) // [第一个值,第二个值]
});

和 watchEffect 对比

  • 懒执行副作用:针对某个值执行副作用
  • watch 可以定义什么状态应该触发 watcher 重新运行
  • watch 可以访问数据变化前后的值,watchEffect 不能

自定义函数 - hooks

  • 将相关的 feature 组合在一起

  • 非常易于重用

  • 界面的需求 - 转化为数据的描述

优点

  • 以函数的形式调用,清楚的了解参数和返回的类型,更好的提示
  • 避免命名冲突
  • 代码逻辑脱离组件存在

示例

<template>
<div v-if="todo.loading">Loading todo</div>
<div v-else>{{ todo.result && todo.result.title }}</div>
</template>

<script lang="ts">
import useURLLoader from '../hooks/useURLLoader'
interface PostProps {
userId: number,
id: number,
title: string,
body: string
}
export default {
props: {
msg: String
},
setup() {
const todo = useURLLoader<PostProps>('url');
todo.result
return {
todo
}
}
}
</script>

<style scoped>

</style>
// useURLLoader.js
import { reactive } from 'vue'; // 响应式对象
import axios from 'axios';
interface DataProps<T> {
result: T | null,
loading: boolean,
loaded: boolean,
error: any
}
const useURLLoader = <T = any>(url: string) => {
const data = reactive<DataProps<T>>({
result: null,
loading: true,
loaded: false,
error: null
})
axios.get(url).then(resp => {
data.result = resp.data;
data.loaded = true;
}).catch(e => {
data.error = e;
}).finally(() => {
data.loaded = false;
});
return data;
};

export default useURLLoader;

对比 React 的自定义 hooks

  • 更新数据的方式
  • 触发的时机
    • 为什么要包裹在 useEffect 中?
    • 删除了会有什么问题?
    • 为什么 Vue3 不需要这样做也可以?

其他自学知识点

  • Teleport
  • Fragment
  • Emits Components Options
  • Global API 修改
  • 语法糖
    • <srcipt setup>
    • <style vars>